package br.unicamp.mc715.bank;

import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

import br.unicamp.mc715.bank.BankOperation.BankOperationResponse;
import br.unicamp.mc715.common.MC715Exception;
import br.unicamp.mc715.common.MC715IOException;
import br.unicamp.mc715.common.MC715SerializationException;
import br.unicamp.mc715.middleware.Middle;

public class BankServer extends Thread implements Runnable {
	
	/**O middleware usado para comunicação com as outras instancias*/
	private Middle middle;
	/** Número de contas deste banco*/
	int numberOfAccounts;
	/** Saída para escrever no log */
	private transient Logger logger;
	/** Poll de threads que executam as ExecOps*/
	Executor threadPool = Executors.newFixedThreadPool(10);

	/** Contas deste banco
	 * @see #numberOfAccounts */
	private ArrayList<BankAccount> accounts;

	/** Instancia um novo servidor de banco
 	 * @param numberOfAccounts Número de contas cadastradas neste banco. Deve ser igual para todas os nós do cluster.
 	 * @param nInstance Número de instancias executando neste cluster
	 * @throws IOException Se não puder gravar o Log
	 * */
	public BankServer(int numberOfAccounts, int nInstance) throws MC715Exception, IOException {
		this.numberOfAccounts = numberOfAccounts;
		this.logger = Logger.getLogger("BankServer");

		this.accounts = new ArrayList<BankAccount>();
		for (int i=0; i < numberOfAccounts; i++) {
			BankAccount acc = new BankAccount(i);
			accounts.add(acc);
		}

		this.middle = new Middle(this.numberOfAccounts, nInstance);
		
		logger.info("Servidor rodando em " + middle.getTransportId());
		
		// Escreve os saldos e históricos de todas as contas ao encerrar o programa
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				int numOps = 0;
				StringBuffer logstr = new StringBuffer("LOGS:\n");
				for(BankAccount account : accounts) {
					numOps += account.getLog().size();
					logstr.append("* " + account + "\n");
					logstr.append(account.getLogAsString() + "\n");
				}
				logstr.append("* TOTAL: " + numOps + " operações");
				logger.info(logstr);
			}
		});
	}

	/** 
	 * Classe responsável por executar, de forma assíncrona (Em outra thread), uma operação.
	 * 
	 * Para garantir que não existam diversas instâncias atuando na mesma conta, é necessário obter
	 * acesso à sessão crítica em {@link BankAccount#sem} ANTES de criar o {@link ExecOp}.
	 * 
	 * A sessão crítica é liberada ao fim do processamento.
	 */
	private class ExecOp implements Runnable
	{
		/**Operação que deve ser executada*/
		private BankOperation operation;
		/**Conta na qual a operação deve ser executada*/
		private BankAccount account;

		/**Cria um {@link ExecOp} 
		 * @param operacao Operação que deve ser executada
		 * @param conta Conta que deve ser afetada pela operação */
		public ExecOp(BankOperation operacao, BankAccount conta)
		{
			operation = operacao;	// Operacao a ser executada
			account = conta;
		}

		/**
		 * Executa a operação {@link #operation} na conta {@link #account}.
		 * 
		 * Ao fim, libera a seção crítica em {@link BankAccount#sem}. 
		 */
		public void run()
		{
			int result = account.execOp(operation);
			
			/** Envia resposta para o cliente se operacao for balanco */
			if (operation.getOperationType() == 'b')
			{
				try 
				{
					System.out.println("Balance="+result);
					middle.sendResponse(new BankOperationResponse(operation, result));
				} 
				catch (MC715IOException e) {
					e.printStackTrace();
				} 
				catch (MC715SerializationException e) {
					e.printStackTrace();
				}				
			}
			account.sem.release();	// Libera para que a proxima operacao possa ser executada
		}
	}

	/**
	 * Verifica continuamente o estado das filas no middleware
	 * Quando uma operação é encontrada, e a conta especificada não 
	 * estiver bloqueada por outra operação, a conta fica bloqueada
	 * e uma nova thread é iniciada para processar a operação 
	 */
	public void run()
	{
		while(true)
		{
			for(int i = 0; i < numberOfAccounts; i++)
			{
				if (middle.hasOp(i))	// Se existe operacao na conta 'i' 
				{
					BankAccount account = accounts.get(i);	// Conta 'i'

					/* Duas operacoes nao podem ser executadas ao mesmo tempo na mesma conta! */
					if(account.sem.tryAcquire())	// Tenta adiquirir permissao para executar
					{
						BankOperation oper = middle.getOp(i);
						
						if(oper != null)
							// Executa operacao 'op'
							threadPool.execute( new ExecOp(oper, account) );
						else
							account.sem.release();
					}
				}
			}
		}
	}

	/**
	 * Inicia o funcionamento do banco.
	 * Cria uma nova thread para receber e tratar as operações, e, na thread atual,
	 * executa o terminal (iterativo ou automático, de acordo com {@link #bankType}).
	 * @throws IOException Se ocorrer algum erro
	 */
	public void exec() throws IOException {
		this.start();
	}

	/** Método principal, cria e executa um servidor de Bank.
	 * @param args Argumentos de linha de comando
	 * @throws MC715Exception Se ocorrer algum erro
	 * @throws NumberFormatException Se ocorrer algum erro
	 * @throws IOException Se ocorrer algum erro
	 */
	public static void main(String[] args) throws MC715Exception, NumberFormatException, IOException 
	{
		if (args.length != 3) 
		{
			System.out.println("Usage: java -jar mc715server.jar <number of accounts> <stable media> <instances>");
			System.out.println("Usage: <log file> is either a directory or a IP:port.");
			System.out.println("Usage: <instances> is the number of bank processes running.");
			System.exit(0);
		}
		
		//Configura o Log4J
        BasicConfigurator.resetConfiguration();
        BasicConfigurator.configure(new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
        BasicConfigurator.configure(new FileAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN), args[1], false));
		

        BankServer bank = new BankServer(Integer.parseInt(args[0]), Integer.parseInt(args[2]));
        bank.start();
	}
}
